查看原文
其他

完蛋!面试官问我 InnoDB 的物理存储结构!

陈树义 树哥聊编程 2022-10-25

前段时间去面试,面试官突然问我:聊聊 InnoDB 的物理存储结构吧!

树义突然又眼圈一黑,啥都想不起来了!

虽说之前有大致了解过 MySQL,但对 InnoDB 的物理结构,却真的没咋了解过!那么,今天就来聊聊 InnoDB 的物理存储结构吧!

相信很多人都知道逻辑结构和物理结构这两个概念,但是都很好奇它们的区别是什么?

简单地说:所谓物理存储结构,指的是 MySQL 的数据是怎么存储在物理介质上的,由哪些磁盘文件组成。所谓逻辑存储结构,指的是这些数据是如何有结构地组织起来的。

此文所描述的 InnoDB 物理存储结构,依据的是 MySQL 8.0 版本。

从 MySQL 官方文档中,我们可知 InnoDB 在物理层面上可划分为如下 7 个模块:

  • 系统表空间(System Tablespace)
  • 双写缓存文件(Doublewrite Buffer Files)
  • Undo 表空间(Undo Tablespaces)
  • Redo Log 文件(Redo Log)
  • 独占表空间(File-Per-Table Tablespaces)
  • 通用表空间(General Tablespaces)
  • 临时表空间(Temporary Tablespaces)

而在这 7 个模块中,最为关键的是如下三个模块:系统表空间(System Tablespace)、独立表空间(File-Per-Table Tablespaces)、日志文件组(Redo Log)。

InnoDB 磁盘存储结构 MySQL 8.0

系统表空间(System Tablespace)

在开始之前,我们需要理解表空间这个概念。其实表空间,就是存储表的空间之意。它是 InnoDB 用于描述数据存储的术语,大致是存储表相关数据的地方之意。系统表空间,顾名思义就是 InnoDB 这个系统的默认表空间,也叫共享表空间,对应的是 MySQL 数据目录下的 ibdata1 文件。如果我们将 innodb_file_per_table 设置为 off,那么我们使用 CREATE TABLE 语句创建的表数据,都会存储在系统表空间中。

而如果我们将 innodb_file_per_table 设置为 on,则每个表将独立地产生一个表空间文件,以 ibd 结尾,数据、索引、表的内部数据字典信息都将保存在这个单独的表空间文件中。但是还有一些信息,例如 undo log 信息,还是会保存在系统表空间中。例如我们在 test 数据库中创建了一个名为 user 的表,那么就会在 MySQL 的数据文件夹的 test 文件夹下,有一个名为 user.ibd 文件。

我们注意到系统表空间还有一个名为 change buffer 的东西,它到底是啥东西呢?

change buffer,其实在之前版本是插入缓冲(insert buffer),后续版本变成了 change buffer(参考:内幕 2.4.1 章节)。这一块区域就是 Change Buffer。5.5 之前叫 Insert Buffer 插入缓冲,现在也能支持 delete 和 update,最后把 Change Buffer 记录到数据页的操作叫做 merge。

这里面有一句话很关键:除了主键聚合索引外,还产生了一个 name 列的辅助索引,对于该非聚集索引来说,叶子节点的插入不再有序,这时就需要离散访问非聚集索引页,插入性能变低。

这句话的意思是:聚集索引的插入是有序自增的,那么插入的时候直接跟在后面就可以了,不需要再做其他随机访问操作。而由于插入的非聚集索引列,并不是有序的,可能会插在中间,这时候就需要看看中间这个页是否加载到内存里。如果加载了,那还好,那就做 B+ 树操作,调整就行。不在内存里的话,那就麻烦了,需要读取多次 page 到内存。

为了提高效率,就弄了一个 insert buffer,简单地说,就是把多次 insert 或者修改操作,先缓存起来,然后等到差不多的时候,再一起去做,提高效率,避免多次 IO 读取。

插入缓冲,并不是缓存的一部分,而是物理页,对于非聚集索引的插入或更新操作,不是每一次直接插入索引页。而是先判断插入的非聚集索引页是否在缓冲池中。如果在,则直接插入,如果不再,则先放入一个插入缓冲区中。然后再以一定的频率执行插入缓冲和非聚集索引页子节点的合并操作。

当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。

双写缓存文件(Doublewrite Buffer Files)

如果说插入缓冲带给 InnoDB 存储引擎的是性能,那么双写带给 InnoDB 存储引擎的是数据的可靠性。当数据库宕机时,可能数据库正在写一个页面,而这个页只写了一部分(比如 16K 的页,只写了前 4K 的页),我们称之为写失效。在 InnoDB 存储引擎未使用 double write 之前,曾出现过因为部分写失效而导致数据丢失的情况。

InnoDB 存储引擎 double write 的体系架构如下图所示:

双写缓存(double write)示意图

double write 由两部分组成:一部分是内存中的 double write buffer,大小为 2MB。另一部分是物理磁盘上共享表空间中连续的 128 个页,即两个区,大小同样为 2MB。

当缓冲池的脏页刷新时,并不直接写磁盘,而是会他通过 memcpy 函数将脏页先拷贝到内存中的 doublewrite buffer,之后通过 doublewrite buffer 再分两次,每次写入 1MB 到共享表空间的物理磁盘上,然后马上调用 fsync 函数,同步磁盘,避免缓冲写带来的问题。在这个过程中,因为 doublewrite 页是连续的,因此这个过程是顺序写,开销不是很大。

在完成 doublewrite 页的写入之后,再将 doublewrite buffer 中的页写入各个表空间文件中,此时的写入则是离散的。我们可以通过 show global status like 'innodb_dblwr%'\G 观察具体情况。

所以说双写,其实指的是刷脏页的时候,即会将脏页数据写入共享表空间的 2 个页中,也会写入具体的表空间中,共享表空间的数据起到备份作用。

如果操作系统在将页写入具体的表空间过程中崩溃了,在恢复过程中,InnoDB 存储引擎可以从共享表空间中的 doublewrite 中找到该页的一个副本,将其拷贝到表空间文件,再应用重做日志。

Undo 表空间(Undo Tablespaces)

undo Log 的数据默认在系统表空间 ibdata1 文件中,因为共享表空间不会自动收缩,也可以单独创建一个 undo 表空间。

一些疑问,关于 rollback segment 与 undo log?

很多时候,我们不太懂,为啥一些说 undo log 在系统表空间,而官方的图却有 undo tablespace ?下面这段话,描述得很清楚,原因是版本变化!

Rollback Segment(rseg)称为回滚段。Mysql5.6 之前 undo 默认记录到系统表空间(ibdata),如果开启了 innodb_file_per_table ,将放在每个表的.ibd 文件中。5.6 之后还可以创建独立的 undo 表空间,8 之后更是默认打开独立 undo 表空间,最低数量为 2,这样才能保证至少一个 undo 表空间进行 truncate,一个 undo 表空间继续使用。

每个 rollback Segment 中默认有 1024 个 undo log segment,mysql5.5 后 1 个 undo 表空间支持 128 个 rollback Segment。0 号 rollback Segment 默认在系统表空间 ibdata 中,1-32rollback Segment 在临时表空间,33~128 在独立 undo 表空间中(没有打开则在系统表空间 ibdata 中,这样系统表空间会太大),所以 1 个表空间最多支持 96*1024 个事务,超了就报错啦。

一个 undo log segment 称为 undo log 或 undo slot 或 undo;一个 undo log 对象对应多个 undo log record,也就是记录的历史版本。(Rollback segment -> 多个 Undo log segment (undo log) -> 多个 undo log record)。

疑问 2:mysql rollback segment 和 undo segment 区别

感觉这两个应该是差不多的呀?

结论:Rollback segment 与 undo segment 是包含的关系,每个 Rollback segment 有 1024 个 undo segment。

undo log 有两个作用:提供回滚和多个行版本控制 (MVCC)。

在数据修改的时候,不仅记录了 redo,还记录了相对应的 undo,如果因为某些原因导致事务失败或回滚了,可以借助该 undo 进行回滚。

undo log 和 redo log 记录物理日志不一样,它是逻辑日志。可以认为当 delete 一条记录时,undo log 中会记录一条对应的 insert 记录,反之亦然,当 update 一条记录时,它记录一条对应相反的 update 记录。

rollback segment 称为回滚段,每个回滚段中有 1024 个 undo log segment。

Redo Log 文件(Redo Log)

redo log 即重做日志,从字面意思来看,其表示可以将事情重做一遍的意思。而事实上,它确实是代表着这个意思。对于上文的更新语句 update t set c=c+1 where id = 2,我们的正常实现思路应该是:

  1. 找到 id 为 2 的记录,取出其 c 字段的值。
  2. 将 c 字段的值加一,之后将 id 为 2 的字段的 c 字段更新。

但实际上 MySQL 并不是这么做的,因为上述这种实现方式虽然能实现,但是每次都要去读取磁盘查找记录、写入磁盘更新记录,整个过程的磁盘 IO 成本很高。为了提高效率,MySQL 使用了一种叫做 WAL(Write-Ahead Logging)的技术,即写之前先记录变更日志(redo log),等待合适的时间再将其变更应用到数据库里。因为我们将操作记录下来了,所以我们可以复现这个操作,这就好像我们将事情重现了一样,因此叫 redo log。

使用 WAL 技术,上面这条更新语句的大致实现思路就变成了:

  1. 记录下更新操作日志:其要将 id 为 2 的记录的 c 字段加 1。
  2. 某个时刻,MySQL 数据库应用这个 redo log 日志,将数据库 id 为 2 的记录的 c 字段加 1。

注意:redo log 并不会应用于磁盘的表空间,而是在重启时应用于内存表空间缓存,用于实现 crash-safe。

可以看到,使用 WAL 技术的方式,可以不需要去读写磁盘,极大提高了执行效率。

我们举一个很形象的例子来理解 WAL 技术。想象有一个酒馆,生意非常好,老板也愿意赊账。每次别人想要赊账,老板都得去翻账本,看看这个人有没有赊过账,有赊账的话就需要在原来的赊账金额上再加上本次消费的金额。

在平时酒馆人不多的时候,这种方式还是可以应付应付的。但是一旦到了酒馆高峰期的时候,每个人都等着结账,这时候再用这种方式去结账,很可能让客户等太久,引起民愤。于是老板想了个办法:我不去账本上找谁赊账了,我直接在黑板上记录下谁赊账了多少钱。例如:张三赊账 3 块银元,李四赊账 4 块银元。

等生意没那么忙的时候,老板拿出账本,将粉板上的变更记录进账本:哦,之前张三赊账了 4 块银元,现在又赊账了 3 块银元,所以张三现在总共赊账 7 块银元。在这个例子中,账本就相当于我们的 MySQL 数据库,粉板就相当于我们的 redo log,它将消费记录保存下来。

独占表空间(File-Per-Table Tablespaces)

如果我们将 innodb_file_per_table 设置为 on,则每个表将独立地产生一个表空间文件,以 ibd 结尾,数据、索引、表的内部数据字典信息都将保存在这个单独的表空间文件中。但是还有一些信息,例如 undo log 信息,还是会保存在系统表空间中。例如我们在 test 数据库中创建了一个名为 user 的表,那么就会在 MySQL 的数据文件夹的 test 文件夹下,有一个名为 user.ibd 文件。

通用表空间(General Tablespaces)

通用表空间是后续的 MySQL 推出的表空间,其与系统表空间类似,可以用于存储表的数据和索引。其作用是可以将一些业务逻辑不同的表,存放在这个通用表空间中,从而达到物理隔离的作用。

临时表空间(Temporary Tablespaces)

存储临时表的数据,包括用户创建的临时表,和磁盘的内部临时表。对应数据目录下的 ibtmp1 文件。当数据服务器正常关闭时,该表空间被删除,下次重新产生。


参考资料:
  • MySQL :: MySQL 5.7 Reference Manual :: 14.6.3 Tablespaces
  • InnoDB 内存结构和磁盘结构_wang2963973852 的博客 - CSDN 博客
  • [Mysql] 漫游 undo log | 土川的自留地
  • 详细分析 MySQL 事务日志 (undo log) - 裸奔的小鸵鸟 - 博客园





推荐阅读








您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存